let nWise = (n, array) => {
  let iterators = Array(n)
    .fill()
    .map(() => array[Symbol.iterator]())
  iterators.forEach((it, index) =>
    Array(index)
      .fill()
      .forEach(() => it.next())
  )
  return Array(array.length - n + 1)
    .fill()
    .map(() => iterators.map((it) => it.next().value))
}

let pairWise = (array) => nWise(2, array)

let sum = (arr) => arr.reduce((a, b) => a + b)

let range = (n) => [...Array(n).keys()]

let rand = (min, max) => Math.random() * (max - min) + min

Array.prototype.last = function () {
  return this[this.length - 1]
}

let flatten = (array) =>
  array.reduce((flat, toFlatten) => flat.concat(Array.isArray(toFlatten) ? flatten(toFlatten) : toFlatten), [])
function FCNN(d3) {
  let randomWeight = () => Math.random() * 2 - 1

  /////////////////////////////////////////////////////////////////////////////
  ///////    Variables    ///////
  /////////////////////////////////////////////////////////////////////////////

  var w = window.innerWidth
  var h = window.innerHeight

  var svg = d3.select("#graph-container").append("svg").attr("xmlns", "http://www.w3.org/2000/svg")

  var g = svg.append("g")
  g.style("transform", "translate(-44%, -21%) scale(1)")
  svg.style("cursor", "move")

  var config = { attributes: true }

  var callback = function (mutationsList) {
    mutationsList.forEach(function (item, index) {

      if (item.type == "childList") {
        console.log("有节点发生改变，当前节点的内容是：")
        console.log(item.target.innerHTML)
      } else if (item.attributeName == "transform") {
        g.style('transform', '')
      }
    })
  }
  // 创建一个链接到回调函数的观察者实例
  var observer = new MutationObserver(callback)

  // 开始观察已配置突变的目标节点
  observer.observe(g['_groups'][0][0], config)

  // 停止观察
  //observer.disconnect();

  var edgeWidthProportional = false
  var edgeWidth = 0.5
  var weightedEdgeWidth = d3.scaleLinear().domain([0, 1]).range([0, edgeWidth])

  var edgeOpacityProportional = false
  var edgeOpacity = 1.0
  var weightedEdgeOpacity = d3.scaleLinear().domain([0, 1]).range([0, 1])

  var edgeColorProportional = false
  var defaultEdgeColor = "#505050"
  var negativeEdgeColor = "#0000ff"
  var positiveEdgeColor = "#ff0000"
  var weightedEdgeColor = d3.scaleLinear().domain([-1, 0, 1]).range([negativeEdgeColor, "white", positiveEdgeColor])

  var nodeDiameter = 20
  var nodeColor = "#ffffff"
  var nodeBorderColor = "#333333"

  var betweenLayers = 160

  var architecture = [8, 12, 8]
  var betweenNodesInLayer = [20, 20, 20]
  var graph = {}
  var layer_offsets = []
  var largest_layer_width = 0
  var nnDirection = "right"
  var showBias = false
  var showLabels = true
  var showArrowheads = false
  var arrowheadStyle = "empty"
  var bezierCurves = false
  var color = "#0000ff"

  let sup_map = { 0: "⁰", 1: "¹", 2: "²", 3: "³", 4: "⁴", 5: "⁵", 6: "⁶", 7: "⁷", 8: "⁸", 9: "⁹" }
  let sup = (s) => Array.prototype.map.call(s, (d) => (d in sup_map && sup_map[d]) || d).join("")

  let textFn = (layer_index, layer_width) => {
    let str = ''
    if (layer_index === 0) {
      str = 'Input' + " Layer ∈ ℝ" +
        sup(layer_width.toString())
    } else if (layer_index === architecture.length - 1) {
      str = 'Output' + " Layer ∈ ℝ" +
        sup(layer_width.toString())
    } else {
      if (layer_index < 3) {
        str = 'Hidden' + " Layer ∈ ℝ" +
          sup(layer_width.toString())
      } else if (layer_index === 3) {
        str = '...'
      }
    }
    return str
  }

  var nominal_text_size = 18
  var textWidth = 70

  var marker = svg
    .append("svg:defs")
    .append("svg:marker")
    .attr("id", "arrow")
    .attr("viewBox", "0 -5 10 10")
    .attr("markerWidth", 7)
    .attr("markerHeight", 7)
    .attr("orient", "auto")

  var arrowhead = marker.append("svg:path").attr("d", "M0,-5L10,0L0,5").style("stroke", defaultEdgeColor)

  var link = g.selectAll(".link")
  var node = g.selectAll(".node")
  var text = g.selectAll(".text")

  /////////////////////////////////////////////////////////////////////////////
  ///////    Methods    ///////
  /////////////////////////////////////////////////////////////////////////////

  /*  处理 graph.nodes  */
  function handleGD(GD) {
    let groupedData = GD.reduce((acc, curr) => {
      let key = curr.layer;
      if (!acc[key]) {
        acc[key] = {
          layer: curr.layer,
          data: []
        };
      }
      acc[key].data.push(curr);

      return acc;
    }, {});

    let sortedData = Object.keys(groupedData).sort((a, b) => a - b).map(layer => ({
      layer: groupedData[layer].layer,
      data: groupedData[layer].data
    }));
    return sortedData
  }
  function handleGraphNodes(nodes) {
    let newNodes = []
    nodes.forEach((n, i) => {
      if (i == 0 || i == nodes.length - 1) {
        newNodes.push(...n.data)
      } else {
        if (n.data.length > 23) {
          newNodes.push(n.data[0])
          newNodes.push({
            color: "black",
            id: n.data[0].layer + "_1",
            layer: n.data[0].layer,
            node_index: 1,
            ellipsis: true,
            num: architecture[n.data[0].layer]
          })
          n.data[n.data.length - 1].node_index = 2
          n.data[n.data.length - 1].id = n.data[n.data.length - 1].layer + '_' + '2'
          newNodes.push(n.data[n.data.length - 1])
        } else {
          newNodes.push(...n.data)
        }
      }
    })
    // console.log(newNodes)
    return newNodes
  }

  function gatherId(nodes) {
    let ids = []
    nodes.forEach(n => {
      ids.push(n.id)
    })
    return ids
  }
  function processArray(arr, architecture) {
    let arr1 = [];
    if (architecture.length > 6) {
      arr.forEach(a => {
        if ((a.source.split('_')[0]) * 1 <= 4 || (a.source.split('_')[0]) * 1 == architecture.length - 1) {
          arr1.push(a)
        }
      })
      for (let i = 0; i < architecture[4]; i++) {
        //最后一层虚线
        arr1.push({ id: `4_${i}-6_0`, source: `4_${i}`, target: `6_0`, weight: randomWeight(), ellipsis: true, })
      }
      return arr1;
    }
  }

  function processNode(nodeArr, architecture) {
    let newNodeArr = [];
    nodeArr.forEach(n => {
      if (n.layer * 1 <= 4 || n.layer * 1 == architecture.length - 1) {
        newNodeArr.push(n)
      }
    })
    return newNodeArr
  }

  function redraw({
    architecture_ = architecture,
    showBias_ = showBias,
    showLabels_ = showLabels,
    bezierCurves_ = bezierCurves
  } = {}) {
    architecture = architecture_
    showBias = showBias_
    showLabels = showLabels_
    bezierCurves = bezierCurves_
    let rangeColor = (layer_index) => {
      return layer_index === 0 ? "red" : layer_index === architecture.length - 1 ? "blue" : "green"
    }
    // let architectureBak = architecture
    graph.nodes = architecture.map((layer_width, layer_index) =>
      range(layer_width).map((node_index) => {
        return {
          id: layer_index + "_" + node_index,
          layer: layer_index,
          node_index: node_index,
          color: rangeColor(layer_index)
        }
      })
    )
    let pnode = pairWise(graph.nodes)
    // console.log(pnode)
    graph.links = pnode.map((nodes) =>
      nodes[0].map((left) =>
        nodes[1].map((right) => {
          return right.node_index >= 0
            ? { id: left.id + "-" + right.id, source: left.id, target: right.id, weight: randomWeight() }
            : null
        })
      )
    )
    graph.nodes = flatten(graph.nodes);
    let sortedData = handleGD(graph.nodes)
    graph.nodes = architecture.length > 6 ? processNode(handleGraphNodes(sortedData), architecture) : handleGraphNodes(sortedData)
    let flattenLinks = architecture.length > 6 ? processArray(flatten(graph.links), architecture) : flatten(graph.links);
    graph.links = flattenLinks.filter(
      (l) =>
        l &&
        (showBias
          ? parseInt(l["target"].split("_")[0]) !== architecture.length - 1
            ? l["target"].split("_")[1] !== "0"
            : true
          : true)
    )
    let links = [];
    let ids = gatherId(graph.nodes)
    graph.nodes.forEach(n => {
      graph.links.forEach(l => {
        if (n.id === l.source && (ids.includes(l.target))) {
          if (n.ellipsis) {
            l.ellipsis = true
          }
          links.push(l)
        }
      })
    })
    graph.links = links
    let label = architecture.map((layer_width, layer_index) => {
      return { id: "layer_" + layer_index + "_label", layer: layer_index, text: textFn(layer_index, layer_width) }
    })

    link = link.data(graph.links, (d) => d.id)
    link.exit().remove()
    link = link.enter().insert("path", ".node").attr("class", "link")
      .attr('from', (d) => d.id.split('_')[0])
      .attr('to', (d) => (d.id.split('_')[0]) * 1 + 1)
      .merge(link)
    node = node.data(graph.nodes, (d) => d.id)
    node.exit().remove()
    node = node
      .enter()
      .append("circle")
      .attr('stroke-dasharray', (n) => {
        if (n.ellipsis) {
          return "2,2"
        }
      })
      .attr('num', (n) => {
        if (n.ellipsis) {
          return n.num
        }
      })
      .attr("r", nodeDiameter / 2)
      .attr("class", "node")
      .attr("id", function (d) {
        return d.id
      })
      .on("mousedown", set_focus)
      .on("mouseup", remove_focus)
      // .on('mouseenter', hover_enter)
      .merge(node)



    text = text.data(label, (d) => d.id)
    text.exit().remove()
    text = text
      .enter()
      .append("text")
      .attr("class", "text")
      .attr("dy", ".35em")
      .attr('id', (d) => d.id)
      .style("font-size", nominal_text_size + "px")
      .merge(text)
      .text(function (d) {
        return showLabels ? d.text : ""
      })

    style()
  }

  function redistribute({
    betweenNodesInLayer_ = betweenNodesInLayer,
    betweenLayers_ = betweenLayers,
    nnDirection_ = nnDirection,
    bezierCurves_ = bezierCurves
  } = {}) {
    betweenNodesInLayer = betweenNodesInLayer_
    betweenLayers = betweenLayers_
    nnDirection = nnDirection_
    bezierCurves = bezierCurves_

    let GD_result = handleGD(graph.nodes);
    if (GD_result.length > 2) {
      architecture = GD_result.reduce((pre, cur) => {
        pre.push(cur.data.length)
        return pre
      }, [])
    }
    let layer_widths = architecture.map(
      (layer_width, i) => layer_width * nodeDiameter + (layer_width - 1) * betweenNodesInLayer[i]
    )
    largest_layer_width = Math.max(...layer_widths)
    layer_offsets = layer_widths.map((layer_width) => (largest_layer_width - layer_width) / 2)
    let indices_from_id = (id) => id.split("_").map((x) => parseInt(x))
    let x = (layer, node_index, text) => {
      if (text === 0) {
        if (layer >= 5) {
          layer = 4
        } else if (layer == 3) {
          layer = 3.5
        }
      } else if (text === 1) { //node
        if (layer > 5) {
          layer = 5
        }
      } else if (text === 2 && layer > 5) {
        layer -= (text - 1)
      }
      let offsetX = (layer) * (betweenLayers + nodeDiameter) + w / 2 - (betweenLayers * layer_offsets.length) / 3
      return offsetX
    }
    let y = (layer, node_index, flag) => {
      if (flag) {
        if (layer > 5) {
          layer = 5
          // layer -= flag
        }
      } else {
        if (layer > 5) {
          layer = 5
        }
      }
      return layer_offsets[layer] + node_index * (nodeDiameter + betweenNodesInLayer[layer]) + h / 2 - largest_layer_width / 2
    }
    let xt = (layer, node_index) =>
      layer_offsets[layer] + node_index * (nodeDiameter + betweenNodesInLayer[layer]) + w / 2 - largest_layer_width / 2
    let yt = (layer, node_index) =>
      layer * (betweenLayers + nodeDiameter) + h / 2 - (betweenLayers * layer_offsets.length) / 3
    if (nnDirection == "up") {
      x = xt
      y = yt
    }
    node
      .attr("cx", function (d) {
        return x(d.layer, d.node_index, 1)
      })
      .attr("cy", function (d) {
        return y(d.layer, d.node_index)
      })
    if (bezierCurves) {
      link.attr('stroke-dasharray', function (d) {
        console.log(d)
      })
      link.attr("d", (d) => {
        let source = [x(...indices_from_id(d.source)), y(...indices_from_id(d.source))]
        let target = [x(...indices_from_id(d.target)), y(...indices_from_id(d.target))]

        // control points
        let cp1 = [(source[0] + target[0]) / 2, source[1]]
        let cp2 = [(source[0] + target[0]) / 2, target[1]]

        return (
          "M" +
          source[0] +
          "," +
          source[1] +
          "C" +
          cp1[0] +
          "," +
          cp1[1] +
          " " +
          cp2[0] +
          "," +
          cp2[1] +
          " " +
          target[0] +
          "," +
          target[1]
        )
      })
    } else {
      link
        .attr(
          "d",
          (d) => {
            // console.log(indices_from_id(d.source))
            return "M" +
              x(...indices_from_id(d.source), 2) +
              "," +
              y(...indices_from_id(d.source)) +
              ", " +
              x(...indices_from_id(d.target), 2) +
              "," +
              y(...indices_from_id(d.target), 2)
          }

        )
        .attr('stroke-dasharray', (n) => {
          if (n.ellipsis) {
            return "2,2"
          }
        })
    }
    text
      .attr("x", function (d) {
        return nnDirection === "right" ? x(d.layer, d.node_index, 0) - textWidth / 2 : w / 2 + largest_layer_width / 2 + 20
      })
      .attr("y", function (d) {
        return nnDirection === "right" ? h / 2 + largest_layer_width / 2 + 20 : y(d.layer, d.node_index)
      })
  }
  function style({
    edgeWidthProportional_ = edgeWidthProportional,
    edgeWidth_ = edgeWidth,
    edgeOpacityProportional_ = edgeOpacityProportional,
    edgeOpacity_ = edgeOpacity,
    negativeEdgeColor_ = negativeEdgeColor,
    positiveEdgeColor_ = positiveEdgeColor,
    edgeColorProportional_ = edgeColorProportional,
    defaultEdgeColor_ = defaultEdgeColor,
    nodeDiameter_ = nodeDiameter,
    nodeColor_ = nodeColor,
    nodeBorderColor_ = nodeBorderColor,
    showArrowheads_ = showArrowheads,
    arrowheadStyle_ = arrowheadStyle,
    bezierCurves_ = bezierCurves,
    color_ = color
  } = {}) {
    // Edge Width
    edgeWidthProportional = edgeWidthProportional_
    edgeWidth = edgeWidth_
    weightedEdgeWidth = d3.scaleLinear().domain([0, 1]).range([0, edgeWidth])
    // Edge Opacity
    edgeOpacityProportional = edgeOpacityProportional_
    edgeOpacity = edgeOpacity_
    // Edge Color
    defaultEdgeColor = defaultEdgeColor_
    edgeColorProportional = edgeColorProportional_
    negativeEdgeColor = negativeEdgeColor_
    positiveEdgeColor = positiveEdgeColor_
    weightedEdgeColor = d3.scaleLinear().domain([-1, 0, 1]).range([negativeEdgeColor, "white", positiveEdgeColor])
    // Node Styles
    nodeDiameter = nodeDiameter_
    nodeColor = nodeColor_
    nodeBorderColor = nodeBorderColor_
    // Arrowheads
    showArrowheads = showArrowheads_
    arrowheadStyle = arrowheadStyle_
    // Bezier curves
    bezierCurves = bezierCurves_
    color = color_

    link.style("stroke-width", function (d) {
      if (edgeWidthProportional) {
        return weightedEdgeWidth(Math.abs(d.weight))
      } else {
        return edgeWidth
      }
    })

    link.style("stroke-opacity", function (d) {
      if (edgeOpacityProportional) {
        return weightedEdgeOpacity(Math.abs(d.weight))
      } else {
        return edgeOpacity
      }
    })

    link.style("stroke", function (d) {
      if (edgeColorProportional) {
        return weightedEdgeColor(d.weight)
      } else {
        return defaultEdgeColor
      }
    })

    link.style("fill", "none")

    link.attr("marker-end", showArrowheads ? "url(#arrow)" : "")
    marker.attr("refX", nodeDiameter * 1.4 + 12)
    arrowhead.style("fill", arrowheadStyle === "empty" ? "none" : defaultEdgeColor)
    node.attr("r", nodeDiameter / 2)
    node.style("fill", nodeColor)

    let nos = node.nodes()
    for (let i = 0; i < nos.length; i++) {
      if (parseInt(nos[i].id.split("_")[0]) == 0) {
        nos[i].style.stroke = "red"
        nos[i].style.fill = "rgba(255,0,0,0.5)"
      } else if (parseInt(nos[i].id.split("_")[0]) == architecture.length - 1) {
        nos[i].style.stroke = "green"
        nos[i].style.fill = "rgba(0,255,0,0.5)"
      } else {
        nos[i].style.stroke = color
      }
    }

    // node.style('color', color)  设置属性
  }
  /////////////////////////////////////////////////////////////////////////////
  ///////    Focus && mouseenter    ///////
  /////////////////////////////////////////////////////////////////////////////
  function splitJoint(label) {
    let str = ''
    label.forEach(l => {
      str += l.text
    })
    d3.select('#graph-container').select('.fixed3')
      .text(str)
  }
  function hover_enter(d) {
    let container = (d3.select('#graph-container').nodes())[0]
    container.querySelector('.fixed3').style.display = "block"
  }
  function set_focus(d) {
    d3.event.stopPropagation()
    node.style("opacity", function (o) {
      return d == o || o.layer == d.layer - 1 ? 1 : 0.1
    })
    link.style("opacity", function (o) {
      return o.target == d.id ? 1 : 0.02
    })
  }
  function remove_focus() {
    d3.event.stopPropagation()
    node.style("opacity", 1)
    link.style("opacity", function () {
      return edgeOpacity
    })
  }
  /////////////////////////////////////////////////////////////////////////////
  ///////    Zoom & Resize   ///////
  /////////////////////////////////////////////////////////////////////////////
  svg.call(
    d3
      .zoom()
      .scaleExtent([1 / 6, 8])
      .on("zoom", zoomed)
  )
  function zoomed() {
    g.attr("transform", d3.event.transform)
    let transformAttr = (g.nodes())[0].attributes.transform.value;
    let res = matchScale(transformAttr)
    if (res !== -1 && res < 1) {
    }
  }
  function matchScale(transformAttr) {
    const regex = /scale\((\d+\.\d+)\)/
    const match = transformAttr.match(regex)
    if (match) {
      const scaleValue = match[1]
      // console.log(scaleValue)
      return scaleValue
    } else {
      return -1
    }
  }
  function resize() {
    w = window.innerWidth
    h = window.innerHeight
    svg.attr("width", w).attr("height", h)
  }
  d3.select(window).on("resize", resize)
  resize()
  /////////////////////////////////////////////////////////////////////////////
  ///////    Return    ///////
  /////////////////////////////////////////////////////////////////////////////
  return {
    redraw: redraw,
    redistribute: redistribute,
    style: style,
    graph: graph,
    link: link
  }
}

window.FCNN = FCNN
